package com.feature50.makeover.utils;

import com.feature50.makeover.utils.ArrayUtils;

import java.lang.reflect.Method;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Array;
import java.util.List;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Arrays;

public class ReflectionUtils {
    private static final String[] PREFIXES_GETTER = { "get", "is", "has" };

    private static final String[] PREFIXES_SETTER = { "set" };

    /**
     * Returns the setter method with a name corresponding to the passed
     * argument. Note that in this method, no parameter matching is used, so for
     * overloading setters, this is dangerous; for that reason, an exception is
     * thrown when multiple setters with the same name exist.
     *
     * @param property
     * @return
     */
    public static Method getSetterMethod(Class type, String property) throws NoSuchMethodException {
        String setterName = getSetterMethodName(property, "set");

        Method setterMethod = null;
        Method[] methods = type.getMethods();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            if (method.getName().equals(setterName)) {
                if (setterMethod != null)
                    throw new IllegalStateException(String.format("Type '%1$s' has more than one method named "
                            + "'%2$s'; use getSetterMethod(Class, String, Class) to obtain the right one", type,
                            setterName));
                setterMethod = method;
            }
        }

        if (setterMethod == null)
            throw new NoSuchMethodException(String.format("No setter method for property '%1$s'", property));

        return setterMethod;
    }

    /**
     * Returns the setter method matching the property specified as well as the
     * argument type.
     *
     * @param type
     * @param property
     * @param parameters
     * @return
     * @throws NoSuchMethodException
     */
    public static Method getSetterMethod(Class type, String property, Class... parameters) throws NoSuchMethodException {
        String setterName = getSetterMethodName(property, "set");
        return type.getMethod(setterName, parameters);
    }

    /**
     * Retrieves a getter method for given property.
     *
     * @param property
     * @return
     */
    public static Method getGetterMethod(Class type, String property) throws NoSuchMethodException {
        String getName = getSetterMethodName(property, "get");
        try {
            return type.getMethod(getName);
        } catch (NoSuchMethodException e) {
            try {
                getName = getSetterMethodName(property, "is");
                return type.getMethod(getName);
            } catch (Exception e1) {
                throw new NoSuchMethodException("No getter found for property '" + property
                        + "' using both 'is' and 'get' prefixes");
            }
        }
    }

    /**
     * Generates a getter/setter/is'er method name for a property
     *
     * @param property
     * @param type
     * @return
     */
    public static String getSetterMethodName(String property, String type) {
        return type + property.substring(0, 1).toUpperCase() + property.substring(1);
    }

    /**
     * Checks if a subclass or any of its superclasses descended from a given
     * base class have declared a specified method. For example, given a class
     * <code>MyForm</code> which is a descendent of <code>Form</code>, you
     * can check to see if <code>MyForm</code> has overridden the method
     * <code>createAndLayoutComponents()</code> using the following
     * invocation:
     * <p>
     * <code>hasMethod("createAndLayoutComponents", null, Form.class, MyForm.class)</code>
     * <p>
     * This method is handy because the reflection API only provides a mechanism
     * to determine (a) if a class has defined a method or if (b) any class if
     * the hierarchy has defined a method. Using this method, you can extend
     * that to check a range of classes in a hierarchy.
     * <p>
     * Note that this method requires that you specify the arguments; if you
     * desire to match on the name only, see {@link }
     *
     * @param methodName
     * @param args
     * @param baseClass
     * @param subClass
     * @return
     */
    public static boolean hasMethod(String methodName, Class[] args, Class baseClass, Class subClass) {
        Class currentClass = subClass;
        while (!currentClass.equals(baseClass)) {
            try {
                Method method = currentClass.getDeclaredMethod(methodName, args);
                if (method != null)
                    return true;
            } catch (NoSuchMethodException e) {
                currentClass = currentClass.getSuperclass();
            }
        }
        return false;
    }

    /**
     * Works just like {@link #hasMethod(String, Class[], Class, Class)} except
     * that it matches based on method name, not on method signature (i.e.,
     * arguments are ignored).
     *
     * @param methodName
     * @param baseClass
     * @param subClass
     * @return
     */
    public static boolean hasMethod(String methodName, Class baseClass, Class subClass) {
        Class currentClass = subClass;
        while (!currentClass.equals(baseClass)) {
            Method[] methods = currentClass.getDeclaredMethods();
            for (int i = 0; i < methods.length; i++) {
                Method method = methods[i];
                String name = method.getName();
                if (name.equals(methodName))
                    return true;
            }
            currentClass = currentClass.getSuperclass();
        }

        return false;
    }

    /**
     * Given a class named FooAction, returns "Foo"
     *
     * @param clazz
     * @return
     */
    public static String getActionName(Class clazz) {
        return getClassNameWithoutSuffix(clazz, "Action");
    }

    /**
     * Given a class named FooForm, returns "Foo"
     *
     * @param clazz
     * @return
     */
    public static String getFormName(Class clazz) {
        return getClassNameWithoutSuffix(clazz, "Form");
    }

    /**
     * Provides the implementation for {@link #getActionName(Class)} and
     * {@link #getFormName(Class)}
     *
     * @param clazz
     * @param suffix
     * @return
     */
    private static String getClassNameWithoutSuffix(Class clazz, String suffix) {
        String name = clazz.getName();
        int lastPeriod = name.lastIndexOf('.');
        String actionName = name.substring(lastPeriod + 1);
        String withoutSuffix = actionName.substring(0, actionName.length() - suffix.length());

        StringBuffer newName = new StringBuffer();
        for (int i = 0; i < withoutSuffix.length(); i++) {
            char c = withoutSuffix.charAt(i);
            if ((Character.isUpperCase(c)) && (i != 0))
                newName.append(" ");
            newName.append(c);
        }

        return newName.toString();
    }

    /**
     * Returns an array of all the setters defined on the instance's class. This
     * method ignores methods defined on any superclass.
     *
     * @param instance
     * @return
     * @see #getGetterMethods(Object)
     */
    public static Method[] getSetterMethods(Object instance) {
        return getSetterMethods(instance.getClass());
    }

    public static Method[] getSetterMethods(Class type) {
        Method[] methods = getMethods(type, "set");
        List<Method> setters = new ArrayList<Method>();
        for (int i = 0; i < methods.length; i++) {
            if (!ArrayUtils.nullOrEmpty(methods[i].getParameterTypes())) {
                if (methods[i].getParameterTypes().length == 1)
                    setters.add(methods[i]);
            }
        }
        return setters.toArray(new Method[0]);
    }

    /**
     * Returns an array of all the getters defined on the instance's class. This
     * method ignores methods defined on any superclass.
     *
     * @param type
     * @return
     * @see #getSetterMethods(Object)
     */
    public static Method[] getGetterMethods(Class type) {
        Method[] methods = getMethods(type, "get");
        List<Method> list = new ArrayList<Method>();
        for (int i = 0; i < methods.length; i++) {
            if (ArrayUtils.nullOrEmpty(methods[i].getParameterTypes()))
                list.add(methods[i]);
        }
        return list.toArray(new Method[0]);
    }

    public static Method[] getGetterMethods(Object instance) {
        return getGetterMethods(instance.getClass());
    }

    private static Method[] getMethods(Class type, String prefix) {
        Method[] methods = type.getMethods();
        List<Method> list = new ArrayList<Method>();

        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            String name = method.getName();
            if ((name.startsWith(prefix)) && (name.length() > 3) && (Character.isUpperCase(name.charAt(3)))) {
                if (ReflectionUtils.hasMethod(method.getName(), method.getParameterTypes(), Object.class, type)) {

                    list.add(method);
                }
            }
        }

        return list.toArray(new Method[list.size()]);
    }

    /**
     * Converts camel case to initial caps and spaces. For example, given a
     * field with a name like firstName, returns a formatted String like "First
     * Name".
     *
     * @param field
     * @return
     */
    public static String getFormattedNameForField(String field) {
        StringBuffer sb = new StringBuffer();
        sb.append(Character.toUpperCase(field.charAt(0)));
        for (int i = 1; i < field.length(); i++) {
            char c = field.charAt(i);
            if (Character.isUpperCase(c))
                sb.append(" ");
            sb.append(c);
        }
        return sb.toString();
    }

    public static Object invokeGetter(Object object, String field) throws NoSuchMethodException,
            IllegalAccessException, InvocationTargetException {
        Method method = getGetterMethod(object.getClass(), field);
        return method.invoke(object);
    }

    /**
     * Invokes the setter method on the passed <code>object</code> that
     * corresponds to the passed <code>field</code>. This method has two code
     * paths. In the first, the type of <code>value</code> parameter is used
     * to match a setter method and is invoked. A second path is used when
     * <code>value</code> is <code>null</code> and therefore its type cannot
     * be inferred. In this path, only the method name of the setter is used to
     * find a setter to invoke. If a setter is overloaded, the correct setter to
     * invoke cannot be determined and this method will necessarily throw an
     * {@link IllegalArgumentException}.
     *
     * @param object
     * @param field
     * @param value
     * @throws NoSuchMethodException
     * @throws IllegalAccessException
     * @throws InvocationTargetException
     */
    public static void invokeSetter(Object object, String field, Object value) throws NoSuchMethodException,
            IllegalAccessException, InvocationTargetException {
        Method method = null;
        if (value != null) {
            try {
                method = getSetterMethod(object.getClass(), field, value.getClass());
            } catch (NoSuchMethodException e) {
                Class primitiveClass = ReflectionUtils.getPrimitiveType(value.getClass());
                if (primitiveClass != null) {
                    try {
                        method = getSetterMethod(object.getClass(), field, primitiveClass);
                    } catch (NoSuchMethodException e1) {
                    }
                }
            }
        }

        if (method == null)
            method = getSetterMethod(object.getClass(), field);

        method.invoke(object, new Object[] { value });
    }

    public static Class getSetterParameterType(Object object, String field) throws NoSuchMethodException {
        Method method = getSetterMethod(object.getClass(), field);
        return method.getParameterTypes()[0];
    }

    /**
     * Converts a method name following the getter/setter convention to the
     * field it represents. For example, it will convert "getFirstName" to
     * "firstName".
     *
     * @param method
     * @return
     */
    public static String getPropertyFromSetter(Method method) {
        return getPropertyFromSetter(method.getName());
    }

    public static String getPropertyFromSetter(String name) {
        for (int i = 0; i < PREFIXES_GETTER.length; i++) {
            if (name.startsWith(PREFIXES_GETTER[i])) {
                return Character.toLowerCase(name.charAt(PREFIXES_GETTER[i].length()))
                        + name.substring(PREFIXES_GETTER[i].length() + 1);
            }
        }

        for (int i = 0; i < PREFIXES_SETTER.length; i++) {
            if (name.startsWith(PREFIXES_SETTER[i])) {
                return Character.toLowerCase(name.charAt(PREFIXES_SETTER[i].length()))
                        + name.substring(PREFIXES_SETTER[i].length() + 1);
            }
        }

        return null;
    }

    // LICENSE: originally copied from Jakarta Commons-BeanUtils project:
    // org.apache.commons.beanutils.MethodUtils, Revision: 189573
    /**
     * Gets the wrapper object class for the given primitive type class. For
     * example, passing <code>boolean.class</code> returns
     * <code>Boolean.class</code>
     *
     * @param primitiveType
     *            the primitive type class for which a match is to be found
     * @return the wrapper type associated with the given primitive or null if
     *         no match is found
     */
    public static Class getPrimitiveWrapper(Class primitiveType) {
        if (boolean.class.equals(primitiveType)) {
            return Boolean.class;
        } else if (float.class.equals(primitiveType)) {
            return Float.class;
        } else if (long.class.equals(primitiveType)) {
            return Long.class;
        } else if (int.class.equals(primitiveType)) {
            return Integer.class;
        } else if (short.class.equals(primitiveType)) {
            return Short.class;
        } else if (byte.class.equals(primitiveType)) {
            return Byte.class;
        } else if (double.class.equals(primitiveType)) {
            return Double.class;
        } else if (char.class.equals(primitiveType)) {
            return Character.class;
        } else {
            return null;
        }
    }

    public static boolean isPrimitiveType(Class type) {
        Class clazz = getPrimitiveWrapper(type);
        return (clazz == null) ? false : true;
    }

    // LICENSE: originally copied from Jakarta Commons-BeanUtils project:
    // org.apache.commons.beanutils.MethodUtils, Revision: 189573
    /**
     * Gets the class for the primitive type corresponding to the primitive
     * wrapper class given. For example, an instance of
     * <code>Boolean.class</code> returns a <code>boolean.class</code>.
     *
     * @param wrapperType
     *            the
     * @return the primitive type class corresponding to the given wrapper
     *         class, null if no match is found
     */
    public static Class getPrimitiveType(Class wrapperType) {
        if (Boolean.class.equals(wrapperType)) {
            return boolean.class;
        } else if (Float.class.equals(wrapperType)) {
            return float.class;
        } else if (Long.class.equals(wrapperType)) {
            return long.class;
        } else if (Integer.class.equals(wrapperType)) {
            return int.class;
        } else if (Short.class.equals(wrapperType)) {
            return short.class;
        } else if (Byte.class.equals(wrapperType)) {
            return byte.class;
        } else if (Double.class.equals(wrapperType)) {
            return double.class;
        } else if (Character.class.equals(wrapperType)) {
            return char.class;
        } else {
            return null;
        }
    }

    // LICENSE: originally copied from Jakarta Commons-BeanUtils project:
    // org.apache.commons.beanutils.MethodUtils#verifyNonPrimitiveClass(Class),
    // Revision: 189573
    /**
     * Find a non primitive representation for given primitive class. Useful to
     * avoid writing this line of code:
     * <p>
     * <code>
     * if (clazz.isPrimitive()) clazz = getPrimitiveWrapper(clazz);
     * </code>
     *
     * @param clazz
     *            the class to find a representation for, not null
     * @return the original class if it not a primitive. Otherwise the wrapper
     *         class. Not null
     */
    public static Class verifyNonPrimitiveClass(Class clazz) {
        if (clazz.isPrimitive()) {
            Class primitiveClazz = ReflectionUtils.getPrimitiveWrapper(clazz);
            if (primitiveClazz != null) {
                return primitiveClazz;
            } else {
                return clazz;
            }
        } else {
            return clazz;
        }
    }

    public static String getClassNameWithoutPackage(Class clazz) {
        return clazz.getName().substring(clazz.getName().lastIndexOf('.') + 1);
    }

    /**
     * Returns all getter methods that return an array or {@link java.util.Collection}
     * implementation.
     *
     * @param clazz
     * @return
     */
    public static Method[] getIndexedGetterMethods(Class clazz) {
        Method[] methods = getGetterMethods(clazz);
        List<Method> methodList = new ArrayList<Method>();
        for (int i = 0; i < methods.length; i++) {
            Method method = methods[i];
            if ((method.getReturnType().isArray()) || (Collection.class.isAssignableFrom(method.getReturnType()))) {
                methodList.add(method);
            }
        }
        return methodList.toArray(new Method[0]);
    }

    /**
     * The advantage of this convenience method for
     * {@link #isIndexedType(Class)} is that if the instance is a {@link Class},
     * it will be cast and passed to <code>isIndexedType(Class)</code> rather
     * than have {@link Object#getClass()} invoked on it.
     *
     * @param instance
     * @return
     */
    public static boolean isIndexedType(Object instance) {
        if (instance instanceof Class) {
            return isIndexedType((Class) instance);
        } else {
            // enum instances are not considered indexed types. the enum class
            // represents the set of all enum instances
            // of that type, but an instance is an element in that collection.
            if (instance.getClass().isEnum())
                return false;

            return isIndexedType(instance.getClass());
        }
    }

    public static boolean isIndexedType(Class type) {
        return ((type.isArray()) || (List.class.isAssignableFrom(type)) || (type.isEnum()));
    }

    public static Object getIndexedType(Object value, Class type) {
        if (value == null)
            return null;

        // handle enum types specially here -- convert them to a list
        if ((value instanceof Class) && ((Class) value).isEnum()) {
            // if the object is an enum, convert it to a list of the enum
            // constants
            Object[] constants = ((Class) value).getEnumConstants();
            value = Arrays.asList(constants);
        }

        if (!isIndexedType(value.getClass()))
            throw new IllegalArgumentException("Cannot convert non-indexed type to indexed type");

        // handle enum types specifically again. if a class is passed in, an
        // enum is valid and is converted to a list.
        // if an enum instance is passed in, it is assumed to be a bug.
        if (value.getClass().isEnum())
            throw new IllegalArgumentException(
                    "An enum instance cannot be converted to an indexed type. Did you mean to convert an enum to a list of values? Pass in an enum class.");

        if ((type.isArray()) && (value.getClass().isArray())) {
            // try and convert the passed array into the type array class if
            // type is more narrow than value's class and
            // the conversion is possible
            if (!type.getClass().equals(value.getClass())) {
                if (value.getClass().isAssignableFrom(type)) {
                    Object[] varray = (Object[]) value;
                    Object[] newArray = (Object[]) Array.newInstance(type.getComponentType(), varray.length);
                    System.arraycopy(varray, 0, newArray, 0, varray.length);
                    return newArray;
                }
            }

            return value;
        }

        if (type.isArray()) {
            List list = (List) value;
            if (list.isEmpty()) {
                return null; // I can't create the right kind of array b/c
                // there are no objects
            } else {
                Object o = list.get(0);
                Class ot = o.getClass();
                Object[] array = (Object[]) Array.newInstance(ot, 0);
                return list.toArray(array);
            }
        }

        if (value instanceof List)
            return value;

        return new ArrayList(Arrays.asList((Object[]) value));
    }

    public static boolean indexedContentsEqual(Object source1, Object source2) {
        if ((source1 == null) && (source2 == null))
            return true;
        if ((source1 == null) || (source2 == null))
            return false;

        Object newSource2 = getIndexedType(source2, source1.getClass());

        if (source1.getClass().isArray()) {
            Object[] a1 = (Object[]) source1;
            Object[] a2 = (Object[]) newSource2;

            if (a1.length != a2.length)
                return false;

            for (int i = 0; i < a1.length; i++)
                if (!a1[i].equals(a2[i]))
                    return false;
            return true;
        } else {
            List l1 = (List) source1;
            List l2 = (List) newSource2;

            return l1.equals(l2);
        }
    }
}